/*
 *  $Id: Connection.cs 1238 2011-03-23 11:39:47Z thenn.erannor $
 *	This file is a part of Ragima CCG.
 *	(C) 2008-2010 Ragima development group.
 *
 *	Licensed under the terms of the GNU GPL License version 2.
 *
 */

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;

namespace Ragima {
    /// <summary>
    /// Класс подключения к удаленному хосту.
    /// </summary>
    public class Connection: IDisposable {
        /// <summary>
        /// Порт по умолчанию
        /// </summary>
        public const int DefaultPort = 13465;
        /// <summary>
        /// Байт - разделитель событий
        /// </summary>
        public const byte TerminateByte = 0;

        private MemoryStream _read, _write;
        private byte[] _buffer; 
        private Socket _socket;
        private TcpListener _listener;

        public Connection() {
            _read = new MemoryStream();
            _write = new MemoryStream();
            _buffer = new byte[256];
            _socket = null;
            _listener = null;
        }

        public void Dispose() {
            if (_socket != null) _socket.Close();
        }

        /// <value>
        /// Есть ли в данный момент подключение.
        /// </value>
        public bool Connected {
            get { return _socket != null && _socket.Connected; }
        }

        /// <value>
        /// Удаленный хост.
        /// </value>
        public EndPoint EndPoint {
            get { return _socket != null ? _socket.RemoteEndPoint : null; }
        }

        /// <summary>
        /// Событие подключения.
        /// </summary>
        public event SimpleEvent OnConnect;
        /// <summary>
        /// Событие отключения.
        /// </summary>
        public event SimpleEvent OnDisconnect;
        /// <summary>
        /// Событие получения события.
        /// </summary>
        public event Action<Events.AbstractEvent> OnReceiveEvent;

        /// <summary>
        /// Устанавливает подключение к указанному хосту
        /// </summary>
        /// <param name="addr">Адрес хоста</param>
        public void Connect(string addr) {
            if (Connected) return;
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            _socket.Connect(addr, DefaultPort);
            DoConnect();
        }

        /// <summary>
        /// Включает ожидание подключений.
        /// </summary>
        /// <returns>Успешно ли включено ожидание</returns>
        public bool WaitConnect() {
            try {
                if (_listener == null) _listener = new TcpListener(IPAddress.Any, DefaultPort);
                _listener.Start();
            } catch {
                _listener = null;
                return false;
            }
            return true;
        }

        /// <summary>
        /// Отменяет ожидание подключений.
        /// </summary>
        public void CancelWaitConnect() {
            if (_listener != null) {
                _listener.Stop();
                _listener = null;
            }
        }

        /// <summary>
        /// Проверяет успешность подключения и вызывает событие.
        /// </summary>
        private void DoConnect() {
            if (!_socket.Connected) {
                _socket.Close();
                _socket = null;
            } else if (OnConnect != null)
                OnConnect();
        }

        /// <summary>
        /// Отключает подключение и вызывает событие.
        /// </summary>
        public void Disconnect() {
            if (_socket == null)
                return;
            if (_socket.Connected)
                _socket.Disconnect(false);
            _socket.Close();
            _socket = null;
            if (OnDisconnect != null)
                OnDisconnect();
        }

        /// <summary>
        /// Отправляет событие.
        /// </summary>
        /// <param name="ev">Отправляемое событие</param>
        public void SendEvent(Events.AbstractEvent ev) {
            if (!Connected)
                return;
            ev.Save(_write);
            _write.WriteByte(TerminateByte);
            SendBuffer();
        }

        /// <summary>
        /// Отправляет подготовленный буфер.
        /// </summary>
        private void SendBuffer() {
            try {
                var buffer = _write.GetBuffer();
                var bytes = (int) _write.Length;
                _socket.Send(buffer, bytes, SocketFlags.None);
                _write.SetLength(0);
            } catch {
                Disconnect();
            }
        }

        /// <summary>
        /// Обрабатывает подключение: проверяет подключение при ожидании и получает событие при имеющемся подключении.
        /// </summary>
        public void ReceiveEvents() {
            if (!Connected) {
                Listen();
                return;
            }
            if (!CanRead())
                return;
            try {
                int bytes;
                while (_socket.Available > 0 && (bytes = _socket.Receive(_buffer)) > 0) {
                    ReadBuffer(bytes);
                }
            } catch {
                Disconnect();
            }
        }

        /// <summary>
        /// Проверяет наличие и устанавливает подключение при ожидании.
        /// </summary>
        private void Listen() {
            if (_listener != null && _listener.Pending()) {
                _socket = _listener.AcceptSocket();
                if (_socket != null) {
                    CancelWaitConnect();
                    DoConnect();
                }
            }
        }

        /// <summary>
        /// Проверяет наличие данных на чтение и проверяет отключение сокета.
        /// </summary>
        /// <returns>Есть ли данные на чтение</returns>
        private bool CanRead() {
            if (!_socket.Poll(0, SelectMode.SelectRead))
                return false;
            if (_socket.Available == 0) {
                Disconnect();
                return false;
            }
            return true;
        }

        /// <summary>
        /// Читает и разбирает буфер.
        /// </summary>
        /// <param name="bytes">Размер буфера</param>
        private void ReadBuffer(int bytes) {
            int p = 0;
            while (p < bytes) {
                int z = Array.IndexOf(_buffer, TerminateByte, p, bytes - p);
                if (z >= 0) {
                    if (z > p) _read.Write(_buffer, p, z - p);
                    p = z;
                    ReadEvent();
                    p++;
                } else {
                    _read.Write(_buffer, p, bytes - p);
                    p = bytes;
                }
            }
        }

        /// <summary>
        /// Читает событие.
        /// </summary>
        private void ReadEvent() {
            _read.Position = 0;
            try {
                var ev = Events.AbstractEvent.Load(_read);
                if (ev != null) {
                    ev.Remote = true;
                    if (OnReceiveEvent != null)
                        OnReceiveEvent(ev);
                }
            } catch (Exception e) {
                Application.ErrorReport(e, false);
            }
            _read.SetLength(0);
        }
    }
}
